package org.qiyu.live.im.core.server.handler.impl;

import com.alibaba.fastjson2.JSON;
import io.netty.channel.ChannelHandlerContext;
import jakarta.annotation.Resource;
import org.idea.qiyu.live.framework.redis.starter.key.IMCoreServerProviderCacheKeyBuilder;
import org.qiyu.live.common.interfaces.enums.ImCoreServerIPRedisKey;
import org.qiyu.live.im.constants.ImConstants;
import org.qiyu.live.im.core.server.common.ChannelHandlerContextCache;
import org.qiyu.live.im.core.server.common.ImContextUtils;
import org.qiyu.live.im.core.server.common.ImMsg;
import org.qiyu.live.im.core.server.common.ImMsgCodeEnum;
import org.qiyu.live.im.core.server.handler.SimpleHandler;
import org.qiyu.live.im.dto.ImMsgBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

import static org.qiyu.live.im.constants.ImConstants.DEFAULT_HEART_BEAT_GAP;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: sfx
 * Date: 2023-10-07
 * Time: 16:49
 */
@Component
public class HeartBeatMsgHandler implements SimpleHandler {

    private final Logger LOGGER = LoggerFactory.getLogger(LoginMsgHandler.class);

    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private IMCoreServerProviderCacheKeyBuilder cacheKeyBuilder;

    /**
     * 基本思路就是 :
     * 1. 我们先要对心跳包做一下校验
     * 2. 心跳包正常我们将实时心跳包记录保存到redis中
     * 3. 每隔30s发送一次心跳,服务端会拿到当前用户ID + appId来去得到redis的key,使用当前时间和存入redis中的时间做一下判断
     * 如果超过30s没有访问,代表心跳记录已经过期,在30s内没有发送心跳-->用户下线了
     *
     * @param ctx
     * @param msg
     */
    @Override
    public void handler(ChannelHandlerContext ctx, ImMsg msg) {
        // 1. 进行心跳包的校验
        Long userId = ImContextUtils.getUserId(ctx);
        Integer appId = ImContextUtils.getAppId(ctx);
        if (userId == null || appId == null) {
            LOGGER.error("attr error,imMsg is {}", msg);
            //有可能是错误的消息包导致，直接放弃连接
            ctx.close();
            throw new IllegalArgumentException("attr is error");
        }

        // 2. 将实时心跳包保存到redis中
        // 2.1 构建redis的key
        String key = cacheKeyBuilder.buildImTokenKey(userId, appId);
        // 2.2 将实时心跳包记录保存到redis的ZSet集合中 key: val-userId score:当前时间戳
        recordOnlineTime(userId, key);

        // 3. 做一些心跳包记录的清理
        removeExpireRecord(key);
        // 4. 给当前key设置过期时间,如果超过时间没有访问,代表已经过期
        // 如果在5min之内进行访问,我们给他续命操作
        redisTemplate.expire(key, 5, TimeUnit.MINUTES);

        // 5. 将消息回写给客户端
        ImMsgBody msgBody = new ImMsgBody();
        msgBody.setUserId(userId);
        msgBody.setAppId(appId);
        msgBody.setData("true");
        ImMsg imMsg = ImMsg.build(ImMsgCodeEnum.IM_HEARTBEAT_MSG.getCode(), JSON.toJSONString(msgBody));

        // 保存当前im-core-server的ip 与 userId之间的绑定关系,设置心跳时间
        stringRedisTemplate.opsForValue().set(ImCoreServerIPRedisKey.ImCoreServerIp + appId + ":" + userId,
                ChannelHandlerContextCache.getImCoreServerAddress(), ImConstants.DEFAULT_HEART_BEAT_GAP * 2, TimeUnit.SECONDS);

        ctx.writeAndFlush(imMsg);
        LOGGER.info("发送心跳成功 imMsg={}", imMsg);
    }

    /**
     * 将实时心跳包记录保存到redis的ZSet集合中 key: val-userId score:当前时间戳
     *
     * @param userId 用户Id
     * @param key    redis的key
     */
    private void recordOnlineTime(Long userId, String key) {
        redisTemplate.opsForZSet().add(key, userId, System.currentTimeMillis());
    }

    /**
     * 将当前时间前30s的心跳记录清除-->这些已经过期了
     * 因为可能会有网络延迟,我们不规定的那么死,如果两次心跳间隔,它都没有进行更新,代表他真的不在线了
     * 我们就将其删除
     *
     * @param key redis的key
     */
    private void removeExpireRecord(String key) {
        redisTemplate.opsForZSet().removeRangeByScore(key, 0, System.currentTimeMillis() - DEFAULT_HEART_BEAT_GAP * 1000 * 2);
    }
}